package br.com.puertorico.common.utils;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;

import org.apache.commons.lang.time.DateFormatUtils;
import org.apache.commons.lang.time.DateUtils;

import br.com.puertorico.common.exceptions.SystemException;
import br.com.puertorico.common.loggin.Logger;

/**
 * Classe utilit&aacute;ria para manipula&ccedil;&atilde;o de datas
 */
public class DateFacility {

	/** Instancia Logger */
	private static Logger _logger = Logger.getLogger(DateFacility.class
			.getName());

	/** Contantes usadas para calcular a diferen&ccedil;a entre datas */
	public static final double DAY_MILLIS = 1000 * 60 * 60 * 24.0015;

	public static final double WEEK_MILLIS = DAY_MILLIS * 7;

	public static final double MONTH_MILLIS = DAY_MILLIS * 30.43675;

	public static final double YEAR_MILLIS = WEEK_MILLIS * 52.2;

	/** Padr&atilde;o dateTime americano yyyy/MM/dd HH:mm */
	public static final String PATTERN_US = "yyyy/MM/dd HH:mm";

	/** Padr&atilde;o dateTime americano yyyy/MM/dd */
	public static final String PATTERN_US_SMALL = "yyyy/MM/dd";

	/** Padr&atilde;o dateTime brasileiro dd/MM/yyyy HH:mm */
	public static final String PATTERN_BR = "dd/MM/yyyy HH:mm";

	/** Padr&atilde;o dateTime brasileiro dd/MM/yyyy */
	public static final String PATTERN_BR_SMALL = "dd/MM/yyyy";

	/**
	 * Dado duas datas, retorna a diferen&ccedil;a delas segundo a unidade em
	 * calUnit.
	 * <p>
	 * Por exemplo:
	 * <ul>
	 * <li>Diferen&ccedil;a em minutos: Calendar.MINUTE</li>
	 * <li>Diferen&ccedil;a em horas: Calendar.HOUR</li>
	 * <li>Diferen&ccedil;a em dias: Calendar.DAY_OF_MONTH</li>
	 * <li>Diferen&ccedil;a em semanas: Calendar.WEEK_OF_YEAR</li>
	 * <li>Diferen&ccedil;a em meses: Calendar.MONTH</li>
	 * <li>Diferen&ccedil;a em anos: Calendar.YEAR</li>
	 * </ul>
	 * </p>
	 *
	 * @param unit
	 * @param then
	 * @param now
	 * @return int A diferen&ccedil;a entre then e now
	 */
	public static Integer getDateDiff(int unit, Date then, Date now) {
		// swap if d1 later than d2
		boolean neg = false;
		if (then != null && now != null) {
			if (then.after(now)) {
				Date temp = then;
				then = now;
				now = temp;
				neg = true;
			}
			// estimate the diff. then is now guaranteed <= now
			int estimate = (int) getEstDiff(unit, then, now);
			// convert the Dates to GregorianCalendars
			GregorianCalendar c1 = new GregorianCalendar();
			c1.setTime(then);
			GregorianCalendar c2 = new GregorianCalendar();
			c2.setTime(now);
			// add 2 units less than the estimate to 1st date,
			// then serially add units till we exceed 2nd date
			c1.add(unit, (int) estimate - 2);
			for (int i = estimate - 1;; i++) {
				c1.add(unit, 1);
				if (c1.after(c2))
					return neg ? 1 - i : i - 1;
			}
		} else {
			return 0;
		}
	}

	/***
	 * Pega a diferen&ccedil;a estimada entre duas datas
	 *
	 * @param calUnit
	 * @param d1
	 * @param d2
	 * @return
	 */
	private static Integer getEstDiff(int calUnit, Date d1, Date d2) {
		long diff = d2.getTime() - d1.getTime();
		switch (calUnit) {
		case Calendar.DAY_OF_MONTH:
			// case Calendar.DATE : // codes to same int as DAY_OF_MONTH
			return (int) (diff / DAY_MILLIS + .5);
		case Calendar.WEEK_OF_YEAR:
			return (int) (diff / WEEK_MILLIS + .5);
		case Calendar.MONTH:
			return (int) (diff / MONTH_MILLIS + .5);
		case Calendar.YEAR:
			return (int) (diff / YEAR_MILLIS + .5);
		default:
			return 0;
		} /* endswitch */
	}

	/**
	 * getDateTime - m&eacute;todo utilizado para pegar Data e Hora do Sistema
	 * conforme o local(BR ou US)
	 *
	 * @param localeStr
	 *            - propriedade locale do arquivo de propriedades. Ex. en, pt
	 * @return String com data/hora
	 * @author <a href="mailto:daniel.camargo@ufms.br">Daniel Camargo</a>
	 */
	public static String getDateTime(String localeStr) {
		Locale locale;
		SimpleDateFormat formato;
		if (localeStr.equalsIgnoreCase("en")) {
			locale = Locale.US;
			formato = new SimpleDateFormat(PATTERN_US);
		} else {
			locale = new Locale("pt", "BR");
			formato = new SimpleDateFormat(PATTERN_BR);
		}
		// pega uma instancia da Classe Calendar de acordo com o locale definido
		// acima.
		Calendar c = Calendar.getInstance(locale);
		Date date = c.getTime();
		// Aplica a formatacao de acordo com Locale da Aplicacao.
		String dataHora = formato.format(date);
		return dataHora;
	}

	/**
	 * Fun&ccedil;&atilde;o utilitaria que adiciona uma determinada quantidade
	 * de dias a uma dada data.
	 *
	 * @param date
	 *            Data na qual se deseja adicionar dias
	 * @param numDays
	 *            Quantidade de dias a ser adicionada, caso o parametro seja
	 *            negativo, a quantidade de dias ser&aacute; subtra&iacute;da
	 * @throws Exception
	 */
	public static void addDay(Date date, int numDays) {
		if (date == null) {
			_logger.error("Parametro n&atilde;o informado corretamente.");
			throw new SystemException(
					"Parametro n&atilde;o informado corretamente.");
		}
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(date);
		calendar.add(Calendar.DAY_OF_MONTH, numDays);
		date.setTime(calendar.getTimeInMillis());
	}

	/**
	 * Funcao utilitaria que adiciona uma determinada quantidade de meses a uma
	 * dada data.
	 *
	 * @param date
	 *            Data na qual se deseja adicionar meses
	 * @param numMonths
	 *            Quantidade de meses a ser adicionada, caso o parametro seja
	 *            negativo, a quantidade de meses ser&aacute; subtra&iacute;da
	 * @throws Exception
	 */
	public static void addMonth(Date date, int numMonths) {
		if (date == null) {
			_logger.error("Parametro n&atilde;o informado corretamente");
			throw new SystemException(
					"Parametro n&atilde;o informado corretamente.");
		}
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(date);
		calendar.add(Calendar.MONTH, numMonths);
		date.setTime(calendar.getTimeInMillis());
	}

	/**
	 * Funcao utilit&aacute;ria que adiciona uma determinada quantidade de meses
	 * a uma dada data.
	 *
	 * @param date
	 *            Data na qual se deseja adicionar meses
	 * @param numYears
	 *            Quantidade de meses a ser adicionada, caso o parametro seja
	 *            negativo, a quantidade de meses sera subtraida
	 * @throws Exception
	 */
	public static void addYear(Date date, int numYears) {
		if (date == null) {
			_logger.error("Parametro n&atilde;o informado corretamente.");
			throw new SystemException(
					"Parametro n&atilde;o informado corretamente.");
		}
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(date);
		calendar.add(Calendar.YEAR, numYears);
		date.setTime(calendar.getTimeInMillis());
	}

	/**
	 * Retorna uma string da data formatada, de acordo com padr&atilde;o
	 * informado
	 *
	 * @param date
	 *            - data a ser formatada
	 * @param pattern
	 *            - padr&atilde;o para data
	 * @return - retorna uma string com a data formatada
	 * @author <a href="mailto:daniel.camargo@ufms.br">Daniel Camargo</a>
	 */
	public static String formatDate(Date date, String pattern) {
		if (date == null || pattern == null) {
			_logger.error("Parametros n&atilde;o informados corretamente.");
			return null;
		}
		try {
			return DateFormatUtils
					.format(date, pattern, new Locale("PT", "BR"));
		} catch (IllegalArgumentException e) {
			_logger.error("Erro ao formatar data", e);
			throw new SystemException(e);
		}
	}

	/**
	 * Formata a data no padr&atilde;o pt_BR (dd/MM/yyyy)
	 *
	 * @param date
	 * @return
	 */
	public static String formatDate(Date date) {
		return formatDate(date, "dd/MM/yyyy");
	}

	/**
	 * Retorna uma data formatada, de acordo com padr&atilde;o informado
	 *
	 * @param date
	 *            - data a ser formatada
	 * @param pattern
	 *            - padr&atilde;o para data
	 * @return retorna a nova data formatada
	 * @author <a href="mailto:daniel.camargo@ufms.br">Daniel Camargo</a>
	 */
	public static Date getDate(String date, String pattern) {
		if (date == null || pattern == null) {
			_logger.error("Parametros n&atilde;o informados corretamente.");
			return null;
		}
		try {
			String[] parsePatterns = { pattern };
			return DateUtils.parseDate(date, parsePatterns);
		} catch (ParseException e) {
			_logger.error("Erro ao formatar data", e);
			throw new SystemException(e);
		}
	}

	/**
	 * M&eacute;todo que retorna uma data incial para uma consulta HQL, usando
	 * na cl&aacute;sula between para obter os resultados corretos por conta dos
	 * campos do tipo datetime.
	 *
	 * @param data
	 * @return retorna a dataInicial preenchiada com hor&aacute;rio default para
	 *         consulta
	 */
	public static Date getDataInicial(Date data) {
		Calendar cal = Calendar.getInstance();
		cal.setTime(data);
		cal.set(Calendar.HOUR_OF_DAY, 0);
		cal.set(Calendar.MINUTE, 0);
		cal.set(Calendar.SECOND, 0);
		return cal.getTime();
	}

	/**
	 * M&eacute;todo que retorna uma data final para uma consulta HQL, usando na
	 * cl&aacute;sula between para obter os resultados corretos por conta dos
	 * campos do tipo datetime.
	 *
	 * @param data
	 * @return retorna a dataFinal preenchiada com hor&aacute;rio default para
	 *         consulta
	 */
	public static Date getDataFinal(Date data) {
		Calendar cal = Calendar.getInstance();
		cal.setTime(data);
		cal.set(Calendar.HOUR_OF_DAY, 23);
		cal.set(Calendar.MINUTE, 59);
		cal.set(Calendar.SECOND, 59);
		return cal.getTime();
	}

	/**
	 * Verifica se a data comparada est&aacute; entre a data inicial e a final.
	 * A igualdade de &eacute; validada como sendo entre os intervalos.
	 *
	 * @param dataComparada
	 *            Data a ser comparada.
	 * @param dataInicial
	 *            Data inicial do intervalo a ser validado.
	 * @param dataFinal
	 *            Data final do intervalo a ser validado.
	 * @return Verdadeiro caso a data comparada esteja entre os intervalos.
	 *         Igualdade tambem &eacute; verdadeiro.
	 */
	public static Boolean entreIntervalo(Date dataComparada, Date dataInicial,
			Date dataFinal) {

		if (dataComparada.before(getDataInicial(dataInicial))
				|| dataComparada.after(getDataFinal(dataFinal))) {
			return false;
		}

		return true;

	}

	/**
	 * M&eacute;todo que recebe um data e verifica se &eacute; maior, menor ou
	 * igual a data atual do sistema.
	 *
	 * @param data
	 * @return {@link Integer} <code>-1</code> se o for uma data menor que a
	 *         atual, <code>0</code> se for a igual a data atual, <code>1</code>
	 *         se for menor que a data atual.
	 */
	public static Integer validarComDataAtual(Date data) throws ParseException {
		SimpleDateFormat sdf = new SimpleDateFormat(PATTERN_BR_SMALL);

		try {
			return sdf.parse(sdf.format(data)).compareTo(
					sdf.parse(sdf.format(new Date())));
		} catch (ParseException e) {
			throw e;
		}
	}

	/**
	 * M&eacute;todo para zerar os valores de tempo de uma data.
	 *
	 * @param data
	 * @return a data sem os valores de hora, minutos, segundos e
	 *         mil&eacute;simos.
	 */
	public static Date zerarTempo(Date data) {
		SimpleDateFormat formatter = new SimpleDateFormat(PATTERN_BR_SMALL);
		try {
			data = formatter.parse(formatter.format(data));
		} catch (ParseException e) {
			throw new IllegalArgumentException("Data invalida");
		}
		return data;
	}

	public static Date prepararPorPadrao(Date data, String pattern) {
		SimpleDateFormat formatter = new SimpleDateFormat(pattern);
		try {
			data = formatter.parse(formatter.format(data));
		} catch (ParseException e) {
			throw new IllegalArgumentException("Data invalida");
		}
		return data;
	}

	public static Date parse(String dateStr) {
		SimpleDateFormat formatter = new SimpleDateFormat(PATTERN_BR_SMALL);
		try {
			return formatter.parse(dateStr);
		} catch (ParseException e) {
			throw new IllegalArgumentException(
					"O formato da data deve ser dd/MM/yyyy");
		}
	}

	public static Date getDataPassada(Integer anosAtras) {
		Calendar c = Calendar.getInstance();
		c.setTime(new Date());
		c.add(Calendar.YEAR, (-1 * anosAtras));
		return c.getTime();
	}

	public static Date avancaParaUltimoMilesimoDoMes(Date date) {
		Calendar c = Calendar.getInstance();
		c.setTime(date);
		c.set(Calendar.DAY_OF_MONTH, c.getActualMaximum(Calendar.DAY_OF_MONTH));
		c.set(Calendar.HOUR_OF_DAY, c.getActualMaximum(Calendar.HOUR_OF_DAY));
		c.set(Calendar.MINUTE, c.getActualMaximum(Calendar.MINUTE));
		c.set(Calendar.SECOND, c.getActualMaximum(Calendar.SECOND));
		c.set(Calendar.MILLISECOND, c.getActualMaximum(Calendar.MILLISECOND));
		return c.getTime();
	}

	public static Date voltaParaPrimeiroMilesimoDoMes(Date date) {
		Calendar c = Calendar.getInstance();
		c.setTime(date);
		c.set(Calendar.DAY_OF_MONTH, c.getActualMinimum(Calendar.DAY_OF_MONTH));
		c.set(Calendar.HOUR_OF_DAY, c.getActualMinimum(Calendar.HOUR_OF_DAY));
		c.set(Calendar.MINUTE, c.getActualMinimum(Calendar.MINUTE));
		c.set(Calendar.SECOND, c.getActualMinimum(Calendar.SECOND));
		c.set(Calendar.MILLISECOND, c.getActualMinimum(Calendar.MILLISECOND));
		return c.getTime();
	}

	public static Date voltaParaUltimoMilesimoDoMesPassado(Date date) {
		Date noMesPassado = voltaParaPrimeiroMilesimoDoMes(date);
		addDay(noMesPassado, -1);
		noMesPassado = avancaParaUltimoMilesimoDoMes(noMesPassado);
		return noMesPassado;
	}

	public static Date voltaParaPrimeiroMilesimoDoMesPassado(Date date) {
		Date noMesPassado = voltaParaPrimeiroMilesimoDoMes(date);
		addDay(noMesPassado, -1);
		noMesPassado = voltaParaPrimeiroMilesimoDoMes(noMesPassado);
		return noMesPassado;
	}

	public static Date avancaParaPrimeiroMilesimoDoMesQueVem(Date date) {
		Date noMesQueVem = avancaParaUltimoMilesimoDoMes(date);
		addDay(noMesQueVem, 1);
		noMesQueVem = voltaParaPrimeiroMilesimoDoMes(noMesQueVem);
		return noMesQueVem;
	}

	public static Date atualizaData(Date date, int field, int amount) {
		Calendar c = Calendar.getInstance();
		c.setTime(date);
		c.add(field, amount);
		return c.getTime();
	}
}
